<?php

class DBConn
{
	// Static instance holder
	private static $instances = array();

	const LOST_CONNECTION_RETRIES = 5;

	private $memcache;
	private $memcache_expiry;
	private $memcache_active = null;
	private $tries = array();
	private $dbs = array();
	private $cache = true;

	public static function destroy($in_kill_specific=false)
	{
		// Clean up connections
		foreach (self::$instances as $pid => $instance)
		{
			// If we are only killing a certain pid, and this is not the right pid, skip onward.
			if ($in_kill_specific !== false && $in_kill_specific !== $pid) continue;

			foreach (self::get()->dbs as $iomodename => &$iomode)
			{
				if ($iomode instanceof MDB2_Driver_mysql)
				{
					//echo "[disc] [".getmypid()."][$pid][$iomodename][$modname] ksp: $in_kill_specific\n";
					$iomode->disconnect();
					self::get($pid)->dbs[$iomodename] = false;
				}
			}
		}
	}

	public static function init($ovr_pid=null)
	{
		$pid = ((!is_null($ovr_pid)) ? $ovr_pid : getmypid());
		if (!isset(self::$instances[$pid]) || is_null(self::$instances[$pid]))
			self::$instances[$pid] = new DBConn();
	}

	public static function &get($provided_pid=null)
	{
		$pid = ((!is_null($provided_pid)) ? $provided_pid : getmypid());
		if (!isset(self::$instances[$pid]) || is_null(self::$instances[$pid]))
			self::init($pid);
		return self::$instances[$pid];
	}

	public static function prepareTries($io_mode)
	{
		// Prepare the failure list if needed
			if (!isset(self::get()->tries[$io_mode]))
			self::get()->tries[$io_mode] = array();
	}

	public static function prepareDBs($io_mode)
	{
		// Prepare the db list if needed
		if (!isset(self::get()->dbs[$io_mode]))
			self::get()->dbs[$io_mode] = array();
	}

	public static function useCache($mode='on')
	{
		self::get()->cache = (($mode == 'on' || $mode == 'yes' || $mode == 1) ? true : false);
	}

	/**
	 * @description Statically returns a reference to the Database connection
	 *
	 * @return MDB2 instance reference
	 */
	public static function &getDB($io_mode="write")
	{
		self::prepareDBs($io_mode);

		if (!is_null(self::get()->dbs[$io_mode]) && !empty(self::get()->dbs[$io_mode]) && self::get()->dbs[$io_mode] !== false)
		{
			$try = self::get()->dbs[$io_mode];
			if ($try instanceof MDB2 || $try instanceof MDB2_Driver_mysql)
			{
				return self::get()->dbs[$io_mode];
			}
		}

		//echo "not found.\n";
		return (self::get()->dbs[$io_mode] = false);
	}

	public static function setDB($io_mode,&$db=false)
	{
		self::prepareDBs($io_mode);
		self::get()->dbs[$io_mode] = $db;
	}

	public static function loadMemcache()
	{
		self::init();
		if (!isset(self::get()->memcache))
		{
			$fr = AFramework::get();
			if (self::get()->memcache_active === false || !$fr->getConfig('memcache active'))
				return false;

			self::get()->memcache_active = ($fr->getConfig('memcache active') ? true : false);
			self::get()->memcache_expiry = strtotime($fr->getConfig('memcache expiry'),0);

			self::get()->memcache = new Memcache();

			$memservers = $fr->getConfig('memcache servers');
			$memservers = explode(' ',$memservers);
			foreach ($memservers as $memserver)
			{
				$memserver = explode('/',$memserver);
				self::get()->memcache->addServer($memserver[0],$memserver[1]);
			}
		}
		return true;
	}

	private static function &connect($in_query_type="write")
	{
		self::init();
		$fr = AFramework::get();

		$in_shown_type = $in_query_type;
		$in_query_type = (($in_query_type == 'elevated_read') ? 'write' : $in_query_type);
		$Database = array_shift(explode(' ',$fr->getConfig('db_auth '.$in_query_type)));

		self::prepareTries($in_query_type);

		$tried_dsn = array();

		while (self::getDB($in_query_type) === false)
		{
			$dsn = self::getBalancedDSN($in_query_type, $fr);
			if ($dsn === false)
				throw new PlatformDatabaseConnectException("Could not connect to any Database servers to perform '$in_shown_type'.");
			array_push($tried_dsn,$dsn);

			$newdb =& MDB2::singleton($dsn['dsn'],$dsn['options']);
			if (PEAR::isError($newdb))
			{
				self::get()->tries[$in_query_type][$dsn['sid']]++;
				continue;
			}
			$newdb->setFetchMode(MDB2_FETCHMODE_ASSOC);
			self::setDB($in_query_type,$newdb);
			break;
		}

		self::get()->tries[$in_query_type] = array();

		$db = self::getDB($in_query_type);

		if ($db === false)
			throw new PlatformDatabaseConnectException("Could not connect to any Database servers to perform '$in_shown_type'.");

		return $db;
	}

	public static function getBalancedDSN($server_type, &$fr)
	{
		$db_type = $fr->getConfig("db type");
		$server_list = $fr->getConfig("db ".$server_type);
		$servers = explode(' ',$server_list);
		$list_size = sizeof($servers);

		// Fail if we have tried all servers.
		if (sizeof(self::get()->tries[$server_type]) == $list_size && array_sum(self::get()->tries[$server_type]) >= (sizeof(self::get()->tries[$server_type]) * self::LOST_CONNECTION_RETRIES))
			return false;

		do
		{
			$choice = mt_rand(0,$list_size-1);
		}
		while(isset(self::get()->tries[$server_type][$choice]) && self::get()->tries[$server_type][$choice] >= self::LOST_CONNECTION_RETRIES);

		$auth = explode(' ',$fr->getConfig('db_auth '.$server_type));

		$server = $servers[$choice];
		$opts = array(
			'persistent'	=> true,
			'debug'			=> 2
		);
		if ($fr->getGuiMode() != "cron")
			$opts['persistent'] = false;

		return array(
				'dsn'		=> array(
						'phptype'		=> $db_type,
						'username'		=> $auth[1],
						'password'		=> $auth[2],
						'hostspec'		=> $server,
						'database'		=> $auth[0]
					),
				'options'	=> $opts,
				'sid'		=> $choice
			);
	}

	public static function insert($in_options)
	{
		$module = "fr";
		if (!is_array($in_options))
		{
			$in_options = array(
				'query'	=> $in_options
			);
		}

		$query = $in_options['query'];

		$db = DBConn::connect('write');

		$result = $db->exec($query);

		if (PEAR::isError($result))
		{
			$cmatch = preg_match("/Native code: ([0-9]+)/",$result->getUserInfo(),$native_matches);
			if ($cmatch)
				$native = $native_matches[1];
			else
				$native = null;

			if (in_array($native,array(2013,2006)) && $in_options['retry'] < self::LOST_CONNECTION_RETRIES)
			{
				self::incrRetry($in_options);
				return self::insert($in_options);
			}
			else
			{
				$msg = (($in_options['retry']) ? "Failed query after ".$in_options['retry']." tries. " : "");
				throw new PlatformRecordSQLErrorException($msg.$result->getMessage()." - ".$result->getUserInfo());
			}
		}

		if (!$result)
			throw new PlatformRecordAlreadyExistsException();

		return $db->lastInsertID();
	}

	public static function exec($in_options)
	{
		$module = "fr";
		if (!is_array($in_options))
		{
			$use_old_db = ((is_bool($in_old)) ? $in_old : $use_old_db);
			$in_options = array(
				'query'	=> $in_options,
				'old'	=> $use_old_db
			);
		}

		$query = $in_options['query'];

		$result = DBConn::connect('write')->exec($query);

		if (PEAR::isError($result))
		{
			$cmatch = preg_match("/Native code: ([0-9]+)/",$result->getUserInfo(),$native_matches);
			if ($cmatch)
				$native = $native_matches[1];
			else
				$native = null;

			if (!is_null($native) && in_array($native,array(2013,2006)) && $in_options['retry'] < self::LOST_CONNECTION_RETRIES)
			{
				self::incrRetry($in_options);
				return self::exec($in_options);
			}
			else
			{
				$msg = (($in_options['retry']) ? "Failed query after ".$in_options['retry']." tries. " : "");
				throw new PlatformRecordSQLErrorException($msg.$result->getMessage()." - ".$result->getUserInfo());
			}
		}

		return (($result) ? $result : 0);
	}

	public static function selectOne($in_options)
	{
		if (!is_array($in_options))
			$in_options = array('query'=>$in_options);
		$in_options['type'] = 'one';
		return self::select($in_options);
	}

	public static function selectAll($in_options)
	{
		if (!is_array($in_options))
			$in_options = array('query'=>$in_options);
		$in_options['type'] = 'all';
		return self::select($in_options);
	}

	public static function selectRow($in_options)
	{
		if (!is_array($in_options))
			$in_options = array('query'=>$in_options);
		$in_options['type'] = 'row';
		return self::select($in_options);
	}

	public static function selectObject($in_options)
	{
		if (!is_array($in_options))
			$in_options = array('query'=>$in_options);
		$in_options['type'] = 'query';
		return self::select($in_options);
	}

	public static function select($in_options)
	{
		// $in_query, $in_save_total_queries=false, $in_old_db=false
		$text_db_name = 'new';
		$queryType = 'queryAll';
		$use_cache = true;
		if (!is_array($in_options))
		{
			$in_options = array('query' => $in_options);
		}

		$query = $in_options['query'];
		$elevate = ((isset($in_options['elevate'])) ? $in_options['elevate'] : false);
		$use_cache = self::get()->cache;
		$use_cache = ((isset($in_options['cache'])) ? $in_options['cache'] : $use_cache);
		switch ($in_options['type'])
		{
			case 'all':
				$queryType = 'queryAll';
				break;
			case 'one':
				$queryType = 'queryOne';
				break;
			case 'row':
				$queryType = 'queryRow';
				break;
			case 'query':
				$queryType = 'query';
				break;
		}

		if ($use_cache && self::get()->memcache_active !== false)
		{
			$memcache_key = self::makeMemcacheKey($query,$text_db_name);
			$cache = self::getCache($memcache_key);
			if ($cache !== false)
			{
				return $cache;
			}
		}

		$micro_s = microtime(true);
		$result = DBConn::connect((($elevate) ? 'elevated_read' : 'read'))->$queryType($query);
		$GLOBALS['qry_time'] += microtime(true) - $micro_s;

		if (PEAR::isError($result))
		{
			$cmatch = preg_match("/Native code: ([0-9]+)/",$result->getUserInfo(),$native_matches);
			if ($cmatch)
				$native = $native_matches[1];
			else
				$native = null;

			if (in_array($native,array(2013,2006)) && $in_options['retry'] < self::LOST_CONNECTION_RETRIES)
			{
				self::incrRetry($in_options);
				return self::select($in_options);
			}
			else
			{
				$msg = (($in_options['retry']) ? "Failed query after ".$in_options['retry']." tries. " : "");
				throw new PlatformRecordSQLErrorException('gc:'.$result->getCode().' !! '.$msg.$result->getMessage()." - ".$result->getUserInfo());
			}
		}

		if ($use_cache && self::get()->memcache_active === true)
		{
			self::cache($memcache_key,$result);
		}

		return $result;
	}

	private static function incrRetry(&$options)
	{
		if (!is_integer($options['retry']))
			$options['retry'] = 0;
		if (is_integer($options['retry']))
			$options['retry'] += 1;
	}

	public static function quote()
	{
		$args = func_get_args();
		return call_user_func_array(array(DBConn::connect("write"),"quote"), $args);
	}

	public static function escape($value)
	{
		return mysql_real_escape_string($value);
	}

	public static function cache($key,$DBConn)
	{
		if (!self::loadMemcache()) return;

		self::get()->memcache->set($key,$DBConn,MEMCACHE_COMPRESSED,self::get()->memcache_expiry);
	}

	public static function rmCache($key)
	{
		if (!self::loadMemcache()) return;

		self::get()->memcache->delete($key);
	}

	public static function getCache($key)
	{
		if (!self::loadMemcache())
		{
			return false;
		}
		return self::get()->memcache->get($key);
	}

	public static function makeMemcacheKey($query,$db_name,$extra=null)
	{
		return $db_name.'-'.md5($query).(($extra != null) ? '-'.$extra : '');
	}

}

class PlatformDatabaseConnectException			extends Exception { }
class PlatformRecordSQLErrorException			extends Exception { }
class PlatformEmptyRecordsetException			extends Exception { }

?>
